#include "../stdafx.h"

#include "../include/Platform.h"

#include "CallTip.h"

namespace soy{

	static const int insetX = 5;    // text inset in x from calltip border
	static const int widthArrow = 14;

	CallTip::CallTip() {
		wCallTip = 0;
		inCallTipMode = false;
		posStartCallTip = 0;
		val = 0;
		rectUp = Rect(0,0,0,0);
		rectDown = Rect(0,0,0,0);
		lineHeight = 1;
		offsetMain = 0;
		startHighlight = 0;
		endHighlight = 0;
		tabSize = 0;
		useStyleCallTip = false;    // for backwards compatibility

#ifdef __APPLE__
		// proper apple colours for the default
		colourBG.desired = Color(0xff, 0xff, 0xc6);
		colourUnSel.desired = Color(0, 0, 0);
#else
		colourBG.desired = Color(0xff, 0xff, 0xff);
		colourUnSel.desired = Color(0x80, 0x80, 0x80);
#endif
		colourSel.desired = Color(0, 0, 0x80);
		colourShade.desired = Color(0, 0, 0);
		colourLight.desired = Color(0xc0, 0xc0, 0xc0);
		codePage = 0;
		clickPlace = 0;
	}

	CallTip::~CallTip() {
		font.Release();
		wCallTip.Destroy();
		delete []val;
		val = 0;
	}

	void CallTip::RefreshColourPalette(Palette &pal, bool want) {
		pal.WantFind(colourBG, want);
		pal.WantFind(colourUnSel, want);
		pal.WantFind(colourSel, want);
		pal.WantFind(colourShade, want);
		pal.WantFind(colourLight, want);
	}

	// Although this test includes 0, we should never see a \0 character.
	static bool IsArrowCharacter(char ch) {
		return (ch == 0) || (ch == '\001') || (ch == '\002');
	}

	// We ignore tabs unless a tab width has been set.
	bool CallTip::IsTabCharacter(char ch) const {
		return (tabSize > 0) && (ch == '\t');
	}

	int CallTip::NextTabPos(int x) {
		if (tabSize > 0) {              // paranoia... not called unless this is true
			x -= insetX;                // position relative to text
			x = (x + tabSize) / tabSize;  // tab "number"
			return tabSize*x + insetX;  // position of next tab
		} else {
			return x + 1;                 // arbitrary
		}
	}

	// Draw a section of the call tip that does not include \n in one colour.
	// The text may include up to numEnds tabs or arrow characters.
	void CallTip::DrawChunk(Surface *surface, int &x, const char *s,
		int posStart, int posEnd, int ytext, Rect rcClient,
		bool highlight, bool draw) {
			s += posStart;
			int len = posEnd - posStart;

			// Divide the text into sections that are all text, or that are
			// single arrows or single tab characters (if tabSize > 0).
			int maxEnd = 0;
			const int numEnds = 10;
			int ends[numEnds + 2];
			for (int i=0; i<len; i++) {
				if ((maxEnd < numEnds) &&
					(IsArrowCharacter(s[i]) || IsTabCharacter(s[i]))) {
						if (i > 0)
							ends[maxEnd++] = i;
						ends[maxEnd++] = i+1;
				}
			}
			ends[maxEnd++] = len;
			int startSeg = 0;
			int xEnd;
			for (int seg = 0; seg<maxEnd; seg++) {
				int endSeg = ends[seg];
				if (endSeg > startSeg) {
					if (IsArrowCharacter(s[startSeg])) {
						bool upArrow = s[startSeg] == '\001';
						rcClient.left = x;
						rcClient.right = rcClient.left + widthArrow;
						if (draw) {
							const int halfWidth = widthArrow / 2 - 3;
							const int centreX = rcClient.left + widthArrow / 2 - 1;
							const int centreY = (rcClient.top + rcClient.bottom) / 2;
							surface->FillRectangle(rcClient, colourBG.allocated);
							Rect rcClientInner(rcClient.left + 1, rcClient.top + 1,
								rcClient.right - 2, rcClient.bottom - 1);
							surface->FillRectangle(rcClientInner, colourUnSel.allocated);

							if (upArrow) {      // Up arrow
								Point pts[] = {
									Point(centreX - halfWidth, centreY + halfWidth / 2),
									Point(centreX + halfWidth, centreY + halfWidth / 2),
									Point(centreX, centreY - halfWidth + halfWidth / 2),
								};
								surface->Polygon(pts, sizeof(pts) / sizeof(pts[0]),
									colourBG.allocated, colourBG.allocated);
							} else {            // Down arrow
								Point pts[] = {
									Point(centreX - halfWidth, centreY - halfWidth / 2),
									Point(centreX + halfWidth, centreY - halfWidth / 2),
									Point(centreX, centreY + halfWidth - halfWidth / 2),
								};
								surface->Polygon(pts, sizeof(pts) / sizeof(pts[0]),
									colourBG.allocated, colourBG.allocated);
							}
						}
						xEnd = rcClient.right;
						offsetMain = xEnd;
						if (upArrow) {
							rectUp = rcClient;
						} else {
							rectDown = rcClient;
						}
					} else if (IsTabCharacter(s[startSeg])) {
						xEnd = NextTabPos(x);
					} else {
						xEnd = x + surface->WidthText(font, s + startSeg, endSeg - startSeg);
						if (draw) {
							rcClient.left = x;
							rcClient.right = xEnd;
							surface->DrawTextTransparent(rcClient, font, ytext,
								s+startSeg, endSeg - startSeg,
								highlight ? colourSel.allocated : colourUnSel.allocated);
						}
					}
					x = xEnd;
					startSeg = endSeg;
				}
			}
	}

	int CallTip::PaintContents(Surface *surfaceWindow, bool draw) {
		Rect rcClientPos = wCallTip.GetClientPosition();
		Rect rcClientSize(0, 0, rcClientPos.right - rcClientPos.left,
			rcClientPos.bottom - rcClientPos.top);
		Rect rcClient(1, 1, rcClientSize.right - 1, rcClientSize.bottom - 1);

		// To make a nice small call tip window, it is only sized to fit most normal characters without accents
		int ascent = surfaceWindow->Ascent(font) - surfaceWindow->InternalLeading(font);

		// For each line...
		// Draw the definition in three parts: before highlight, highlighted, after highlight
		int ytext = rcClient.top + ascent + 1;
		rcClient.bottom = ytext + surfaceWindow->Descent(font) + 1;
		char *chunkVal = val;
		bool moreChunks = true;
		int maxWidth = 0;

		while (moreChunks) {
			char *chunkEnd = strchr(chunkVal, '\n');
			if (chunkEnd == NULL) {
				chunkEnd = chunkVal + strlen(chunkVal);
				moreChunks = false;
			}
			int chunkOffset = chunkVal - val;
			int chunkLength = chunkEnd - chunkVal;
			int chunkEndOffset = chunkOffset + chunkLength;
			int thisStartHighlight = Platform::Maximum(startHighlight, chunkOffset);
			thisStartHighlight = Platform::Minimum(thisStartHighlight, chunkEndOffset);
			thisStartHighlight -= chunkOffset;
			int thisEndHighlight = Platform::Maximum(endHighlight, chunkOffset);
			thisEndHighlight = Platform::Minimum(thisEndHighlight, chunkEndOffset);
			thisEndHighlight -= chunkOffset;
			rcClient.top = ytext - ascent - 1;

			int x = insetX;     // start each line at this inset

			DrawChunk(surfaceWindow, x, chunkVal, 0, thisStartHighlight,
				ytext, rcClient, false, draw);
			DrawChunk(surfaceWindow, x, chunkVal, thisStartHighlight, thisEndHighlight,
				ytext, rcClient, true, draw);
			DrawChunk(surfaceWindow, x, chunkVal, thisEndHighlight, chunkLength,
				ytext, rcClient, false, draw);

			chunkVal = chunkEnd + 1;
			ytext += lineHeight;
			rcClient.bottom += lineHeight;
			maxWidth = Platform::Maximum(maxWidth, x);
		}
		return maxWidth;
	}

	void CallTip::PaintCT(Surface *surfaceWindow) {
		if (!val)
			return;
		Rect rcClientPos = wCallTip.GetClientPosition();
		Rect rcClientSize(0, 0, rcClientPos.right - rcClientPos.left,
			rcClientPos.bottom - rcClientPos.top);
		Rect rcClient(1, 1, rcClientSize.right - 1, rcClientSize.bottom - 1);

		surfaceWindow->FillRectangle(rcClient, colourBG.allocated);

		offsetMain = insetX;    // initial alignment assuming no arrows
		PaintContents(surfaceWindow, true);

#ifndef __APPLE__
		// OSX doesn't put borders on "help tags"
		// Draw a raised border around the edges of the window
		surfaceWindow->MoveTo(0, rcClientSize.bottom - 1);
		surfaceWindow->PenColour(colourShade.allocated);
		surfaceWindow->LineTo(rcClientSize.right - 1, rcClientSize.bottom - 1);
		surfaceWindow->LineTo(rcClientSize.right - 1, 0);
		surfaceWindow->PenColour(colourLight.allocated);
		surfaceWindow->LineTo(0, 0);
		surfaceWindow->LineTo(0, rcClientSize.bottom - 1);
#endif
	}

	void CallTip::MouseClick(Point pt) {
		clickPlace = 0;
		if (rectUp.Contains(pt))
			clickPlace = 1;
		if (rectDown.Contains(pt))
			clickPlace = 2;
	}

	Rect CallTip::CallTipStart(int pos, Point pt, const char *defn,
		const char *faceName, int size,
		int codePage_, int characterSet, Window &wParent) {
			clickPlace = 0;
			delete []val;
			val = 0;
			val = new char[strlen(defn) + 1];
			strcpy(val, defn);
			codePage = codePage_;
			Surface *surfaceMeasure = Surface::Allocate();
			if (!surfaceMeasure)
				return Rect();
			surfaceMeasure->Init(wParent.GetID());
			surfaceMeasure->SetUnicodeMode(SC_CP_UTF8 == codePage);
			surfaceMeasure->SetDBCSMode(codePage);
			startHighlight = 0;
			endHighlight = 0;
			inCallTipMode = true;
			posStartCallTip = pos;
			int deviceHeight = surfaceMeasure->DeviceHeightFont(size);
			font.Create(faceName, characterSet, deviceHeight, false, false);
			// Look for multiple lines in the text
			// Only support \n here - simply means container must avoid \r!
			int numLines = 1;
			const char *newline;
			const char *look = val;
			rectUp = Rect(0,0,0,0);
			rectDown = Rect(0,0,0,0);
			offsetMain = insetX;            // changed to right edge of any arrows
			int width = PaintContents(surfaceMeasure, false) + insetX;
			while ((newline = strchr(look, '\n')) != NULL) {
				look = newline + 1;
				numLines++;
			}
			lineHeight = surfaceMeasure->Height(font);

			// Extra line for border and an empty line at top and bottom. The returned
			// rectangle is aligned to the right edge of the last arrow encountered in
			// the tip text, else to the tip text left edge.
			int height = lineHeight * numLines - surfaceMeasure->InternalLeading(font) + 2 + 2;
			delete surfaceMeasure;
			return Rect(pt.x - offsetMain, pt.y + 1, pt.x + width - offsetMain, pt.y + 1 + height);
	}

	void CallTip::CallTipCancel() {
		inCallTipMode = false;
		if (wCallTip.Created()) {
			wCallTip.Destroy();
		}
	}

	void CallTip::SetHighlight(int start, int end) {
		// Avoid flashing by checking something has really changed
		if ((start != startHighlight) || (end != endHighlight)) {
			startHighlight = start;
			endHighlight = end;
			if (wCallTip.Created()) {
				wCallTip.InvalidateAll();
			}
		}
	}

	// Set the tab size (sizes > 0 enable the use of tabs). This also enables the
	// use of the STYLE_CALLTIP.
	void CallTip::SetTabSize(int tabSz) {
		tabSize = tabSz;
		useStyleCallTip = true;
	}

	// It might be better to have two access functions for this and to use
	// them for all settings of colours.
	void CallTip::SetForeBack(const ColourPair &fore, const ColourPair &back) {
		colourBG = back;
		colourUnSel = fore;
	}


};
